/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.connector;

import java.util.Iterator;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectInstance;
import javax.management.ObjectName;

import org.apache.catalina.core.StandardContext;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.mapper.Mapper;
import org.apache.tomcat.util.http.mapper.MappingData;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.res.StringManager;

/**
 * Mapper listener.
 * 
 * @author Remy Maucherat
 * @author Costin Manolache
 */
public class MapperListener implements NotificationListener {
	private static Log log = LogFactory.getLog(MapperListener.class);

	// ----------------------------------------------------- Instance Variables
	/**
	 * Associated mapper.
	 */
	protected Mapper mapper = null;

	/**
	 * MBean server.
	 */
	protected MBeanServer mBeanServer = null;

	/**
	 * The string manager for this package.
	 */
	private StringManager sm = StringManager.getManager(Constants.Package);

	// It should be null - and fail if not set
	private String domain = "*";
	private String engine = "*";

	// ----------------------------------------------------------- Constructors

	/**
	 * Create mapper listener.
	 */
	public MapperListener(Mapper mapper) {
		this.mapper = mapper;
	}

	// --------------------------------------------------------- Public Methods

	public String getDomain() {
		return domain;
	}

	public void setDomain(String domain) {
		this.domain = domain;
	}

	public String getEngine() {
		return engine;
	}

	public void setEngine(String engine) {
		this.engine = engine;
	}

	/**
	 * Initialize associated mapper.
	 */
	public void init() {

		try {

			mBeanServer = Registry.getRegistry(null, null).getMBeanServer();

			registerEngine();

			// Query hosts
			String onStr = domain + ":type=Host,*";
			ObjectName objectName = new ObjectName(onStr);
			Set set = mBeanServer.queryMBeans(objectName, null);
			Iterator iterator = set.iterator();
			while (iterator.hasNext()) {
				ObjectInstance oi = (ObjectInstance) iterator.next();
				registerHost(oi.getObjectName());
			}

			// Query contexts
			onStr = "*:j2eeType=WebModule,*";
			objectName = new ObjectName(onStr);
			set = mBeanServer.queryMBeans(objectName, null);
			iterator = set.iterator();
			while (iterator.hasNext()) {
				ObjectInstance oi = (ObjectInstance) iterator.next();
				registerContext(oi.getObjectName());
			}

			// Query wrappers
			onStr = "*:j2eeType=Servlet,*";
			objectName = new ObjectName(onStr);
			set = mBeanServer.queryMBeans(objectName, null);
			iterator = set.iterator();
			while (iterator.hasNext()) {
				ObjectInstance oi = (ObjectInstance) iterator.next();
				registerWrapper(oi.getObjectName());
			}

			onStr = "JMImplementation:type=MBeanServerDelegate";
			objectName = new ObjectName(onStr);
			mBeanServer.addNotificationListener(objectName, this, null, null);

		} catch (Exception e) {
			log.warn("Error registering contexts", e);
		}

	}

	/**
	 * unregister this from JMImplementation:type=MBeanServerDelegate
	 */
	public void destroy() {
		try {

			ObjectName objectName = new ObjectName(
					"JMImplementation:type=MBeanServerDelegate");
			mBeanServer.removeNotificationListener(objectName, this);
		} catch (Exception e) {
			log.warn("Error unregistering MBeanServerDelegate", e);
		}
	}

	// ------------------------------------------- NotificationListener Methods

	public void handleNotification(Notification notification,
			java.lang.Object handback) {

		if (notification instanceof MBeanServerNotification) {
			ObjectName objectName = ((MBeanServerNotification) notification)
					.getMBeanName();
			String j2eeType = objectName.getKeyProperty("j2eeType");
			String engineName = null;
			if (j2eeType != null) {
				if ((j2eeType.equals("WebModule"))
						|| (j2eeType.equals("Servlet"))) {
					if (mBeanServer.isRegistered(objectName)) {
						try {
							engineName = (String) mBeanServer.getAttribute(
									objectName, "engineName");
						} catch (Exception e) {
							// Ignore
						}
					}
				}
			}

			// At deployment time, engineName is always = null.
			if ((!"*".equals(domain))
					&& (!domain.equals(objectName.getDomain()))
					&& ((!domain.equals(engineName)) && (engineName != null))) {
				return;
			}
			if (log.isDebugEnabled())
				log.debug("Handle " + objectName + " type : "
						+ notification.getType());
			if (notification.getType().equals(
					MBeanServerNotification.REGISTRATION_NOTIFICATION)) {
				String type = objectName.getKeyProperty("type");
				if ("Host".equals(type)
						&& domain.equals(objectName.getDomain())) {
					try {
						registerHost(objectName);
					} catch (Exception e) {
						log.warn("Error registering Host " + objectName, e);
					}
				}

				if (j2eeType != null) {
					if (j2eeType.equals("WebModule")) {
						try {
							registerContext(objectName);
						} catch (Throwable t) {
							log.warn("Error registering Context " + objectName,
									t);
						}
					} else if (j2eeType.equals("Servlet")) {
						try {
							registerWrapper(objectName);
						} catch (Throwable t) {
							log.warn("Error registering Wrapper " + objectName,
									t);
						}
					}
				}
			} else if (notification.getType().equals(
					MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) {
				String type = objectName.getKeyProperty("type");
				if ("Host".equals(type)
						&& domain.equals(objectName.getDomain())) {
					try {
						unregisterHost(objectName);
					} catch (Exception e) {
						log.warn("Error unregistering Host " + objectName, e);
					}
				}

				if (j2eeType != null) {
					if (j2eeType.equals("WebModule")) {
						try {
							unregisterContext(objectName);
						} catch (Throwable t) {
							log.warn(
									"Error unregistering webapp " + objectName,
									t);
						}
					}
				}
			}
		}

	}

	// ------------------------------------------------------ Protected Methods

	private void registerEngine() throws Exception {
		ObjectName engineName = new ObjectName(domain + ":type=Engine");
		if (!mBeanServer.isRegistered(engineName))
			return;
		String defaultHost = (String) mBeanServer.getAttribute(engineName,
				"defaultHost");
		ObjectName hostName = new ObjectName(domain + ":type=Host," + "host="
				+ defaultHost);
		if (!mBeanServer.isRegistered(hostName)) {

			// Get the hosts' list
			String onStr = domain + ":type=Host,*";
			ObjectName objectName = new ObjectName(onStr);
			Set set = mBeanServer.queryMBeans(objectName, null);
			Iterator iterator = set.iterator();
			String[] aliases;
			boolean isRegisteredWithAlias = false;

			while (iterator.hasNext()) {

				if (isRegisteredWithAlias)
					break;

				ObjectInstance oi = (ObjectInstance) iterator.next();
				hostName = oi.getObjectName();
				aliases = (String[]) mBeanServer.invoke(hostName,
						"findAliases", null, null);

				for (int i = 0; i < aliases.length; i++) {
					if (aliases[i].equalsIgnoreCase(defaultHost)) {
						isRegisteredWithAlias = true;
						break;
					}
				}
			}

			if (!isRegisteredWithAlias && log.isWarnEnabled())
				log.warn(sm.getString("mapperListener.unknownDefaultHost",
						defaultHost));
		}
		// This should probablt be called later
		if (defaultHost != null) {
			mapper.setDefaultHostName(defaultHost);
		}
	}

	/**
	 * Register host.
	 */
	private void registerHost(ObjectName objectName) throws Exception {
		String name = objectName.getKeyProperty("host");
		if (name != null) {
			String[] aliases = (String[]) mBeanServer.invoke(objectName,
					"findAliases", null, null);
			mapper.addHost(name, aliases, objectName);
			if (log.isDebugEnabled())
				log.debug(sm.getString("mapperListener.registerHost", name,
						domain));

		}
	}

	/**
	 * Unregister host.
	 */
	private void unregisterHost(ObjectName objectName) throws Exception {
		String name = objectName.getKeyProperty("host");
		mapper.removeHost(name);
		if (log.isDebugEnabled())
			log.debug(sm.getString("mapperListener.unregisterHost", name,
					domain));
	}

	/**
	 * Register context.
	 */
	private void registerContext(ObjectName objectName) throws Exception {

		String name = objectName.getKeyProperty("name");

		// If the domain is the same with ours or the engine
		// name attribute is the same... - then it's ours
		String targetDomain = objectName.getDomain();
		if (!domain.equals(targetDomain)) {
			try {
				targetDomain = (String) mBeanServer.getAttribute(objectName,
						"engineName");
			} catch (Exception e) {
				// Ignore
			}
			if (!domain.equals(targetDomain)) {
				// not ours
				return;
			}
		}

		String hostName = null;
		String contextName = null;
		if (name.startsWith("//")) {
			name = name.substring(2);
		}
		int slash = name.indexOf("/");
		if (slash != -1) {
			hostName = name.substring(0, slash);
			contextName = name.substring(slash);
		} else {
			return;
		}
		// Special case for the root context
		if (contextName.equals("/")) {
			contextName = "";
		}

		if (log.isDebugEnabled())
			log.debug(sm.getString("mapperListener.registerContext",
					contextName));

		Object context = mBeanServer.invoke(objectName, "findMappingObject",
				null, null);
		// mBeanServer.getAttribute(objectName, "mappingObject");
		javax.naming.Context resources = (javax.naming.Context) mBeanServer
				.invoke(objectName, "findStaticResources", null, null);
		// mBeanServer.getAttribute(objectName, "staticResources");
		String[] welcomeFiles = (String[]) mBeanServer.getAttribute(objectName,
				"welcomeFiles");

		mapper.addContext(hostName, contextName, context, welcomeFiles,
				resources);

	}

	/**
	 * Unregister context.
	 */
	private void unregisterContext(ObjectName objectName) throws Exception {

		String name = objectName.getKeyProperty("name");

		// If the domain is the same with ours or the engine
		// name attribute is the same... - then it's ours
		String targetDomain = objectName.getDomain();
		if (!domain.equals(targetDomain)) {
			try {
				targetDomain = (String) mBeanServer.getAttribute(objectName,
						"engineName");
			} catch (Exception e) {
				// Ignore
			}
			if (!domain.equals(targetDomain)) {
				// not ours
				return;
			}
		}

		String hostName = null;
		String contextName = null;
		if (name.startsWith("//")) {
			name = name.substring(2);
		}
		int slash = name.indexOf("/");
		if (slash != -1) {
			hostName = name.substring(0, slash);
			contextName = name.substring(slash);
		} else {
			return;
		}
		// Special case for the root context
		if (contextName.equals("/")) {
			contextName = "";
		}

		// Don't un-map a context that is paused
		MessageBytes hostMB = MessageBytes.newInstance();
		hostMB.setString(hostName);
		MessageBytes contextMB = MessageBytes.newInstance();
		contextMB.setString(contextName);
		MappingData mappingData = new MappingData();
		mapper.map(hostMB, contextMB, mappingData);
		if (mappingData.context instanceof StandardContext
				&& ((StandardContext) mappingData.context).getPaused()) {
			return;
		}

		if (log.isDebugEnabled())
			log.debug(sm.getString("mapperListener.unregisterContext",
					contextName));

		mapper.removeContext(hostName, contextName);

	}

	/**
	 * Register wrapper.
	 */
	private void registerWrapper(ObjectName objectName) throws Exception {

		// If the domain is the same with ours or the engine
		// name attribute is the same... - then it's ours
		String targetDomain = objectName.getDomain();
		if (!domain.equals(targetDomain)) {
			try {
				targetDomain = (String) mBeanServer.getAttribute(objectName,
						"engineName");
			} catch (Exception e) {
				// Ignore
			}
			if (!domain.equals(targetDomain)) {
				// not ours
				return;
			}

		}

		String wrapperName = objectName.getKeyProperty("name");
		String name = objectName.getKeyProperty("WebModule");

		String hostName = null;
		String contextName = null;
		if (name.startsWith("//")) {
			name = name.substring(2);
		}
		int slash = name.indexOf("/");
		if (slash != -1) {
			hostName = name.substring(0, slash);
			contextName = name.substring(slash);
		} else {
			return;
		}
		// Special case for the root context
		if (contextName.equals("/")) {
			contextName = "";
		}
		if (log.isDebugEnabled())
			log.debug(sm.getString("mapperListener.registerWrapper",
					wrapperName, contextName));

		String[] mappings = (String[]) mBeanServer.invoke(objectName,
				"findMappings", null, null);
		Object wrapper = mBeanServer.invoke(objectName, "findMappingObject",
				null, null);

		for (int i = 0; i < mappings.length; i++) {
			boolean jspWildCard = (wrapperName.equals("jsp") && mappings[i]
					.endsWith("/*"));
			mapper.addWrapper(hostName, contextName, mappings[i], wrapper,
					jspWildCard);
		}

	}

}
